module("luci.passwall2.util_sing-box", package.seeall)
local api = require "luci.passwall2.api"
local uci = api.uci
local sys = api.sys
local jsonc = api.jsonc
local appname = api.appname
local fs = api.fs
local CACHE_PATH = api.CACHE_PATH
local split = api.split

local local_version = api.get_app_version("sing-box"):match("[^v]+")
local version_ge_1_11_0 = api.compare_versions(local_version, ">=", "1.11.0")
local version_ge_1_12_0 = api.compare_versions(local_version, ">=", "1.12.0")

local new_port

local function get_new_port()
	if new_port then
		new_port = tonumber(sys.exec(string.format("echo -n $(/usr/share/%s/app.sh get_new_port %s tcp)", appname, new_port + 1)))
	else
		new_port = tonumber(sys.exec(string.format("echo -n $(/usr/share/%s/app.sh get_new_port auto tcp)", appname)))
	end
	return new_port
end

function gen_outbound(flag, node, tag, proxy_table)
	local result = nil
	if node then
		local node_id = node[".name"]
		if tag == nil then
			tag = node_id
		end

		local proxy_tag = nil
		local fragment = nil
		local record_fragment = nil
		local run_socks_instance = true
		if proxy_table ~= nil and type(proxy_table) == "table" then
			proxy_tag = proxy_table.tag or nil
			fragment = proxy_table.fragment or nil
			record_fragment = proxy_table.record_fragment or nil
			run_socks_instance = proxy_table.run_socks_instance
		end

		if node.type ~= "sing-box" then
			local relay_port = node.port
			new_port = get_new_port()
			local config_file = string.format("%s_%s_%s.json", flag, tag, new_port)
			if tag and node_id and tag ~= node_id then
				config_file = string.format("%s_%s_%s_%s.json", flag, tag, node_id, new_port)
			end
			if run_socks_instance then
				sys.call(string.format('/usr/share/%s/app.sh run_socks "%s"> /dev/null',
					appname,
					string.format("flag=%s node=%s bind=%s socks_port=%s config_file=%s relay_port=%s",
						new_port, --flag
						node_id, --node
						"127.0.0.1", --bind
						new_port, --socks port
						config_file, --config file
						(proxy_tag and relay_port) and tostring(relay_port) or "" --relay port
						)
					)
				)
			end
			node = {
				protocol = "socks",
				address = "127.0.0.1",
				port = new_port
			}
		else
			if proxy_tag then
				node.detour = proxy_tag
			end
		end

		result = {
			_id = node_id,
			_flag = flag,
			_flag_proxy_tag = proxy_tag,
			tag = tag,
			type = node.protocol,
			server = node.address,
			server_port = tonumber(node.port),
			domain_strategy = node.domain_strategy,
			detour = node.detour,
		}

		local tls = nil
		if node.tls == "1" then
			local alpn = nil
			if node.alpn and node.alpn ~= "default" then
				alpn = {}
				string.gsub(node.alpn, '[^' .. "," .. ']+', function(w)
					table.insert(alpn, w)
				end)
			end
			tls = {
				enabled = true,
				disable_sni = (node.tls_disable_sni == "1") and true or false, -- Do not send the server name in ClientHello.
				server_name = node.tls_serverName, -- Used to verify the hostname on the returned certificate, unless the settings are insecure. It is also included in ClientHello to support virtual hosts, unless it is an IP address.
				insecure = (node.tls_allowInsecure == "1") and true or false, -- Accepts any server certificate.
				alpn = alpn, -- A list of supported application layer protocols, arranged in order of priority. If both peers support ALPN, the protocol selected will be one of these protocols; otherwise, the connection will fail.
				--max_version = "1.3",
				fragment = fragment,
				record_fragment = record_fragment,
				ech = {
					enabled = (node.ech == "1") and true or false,
					config = node.ech_config and split(node.ech_config:gsub("\\n", "\n"), "\n") or {},
					pq_signature_schemes_enabled = node.pq_signature_schemes_enabled and true or false,
					dynamic_record_sizing_disabled = node.dynamic_record_sizing_disabled and true or false
				},
				utls = {
					enabled = (node.utls == "1" or node.reality == "1") and true or false,
					fingerprint = node.fingerprint or "chrome"
				},
				reality = {
					enabled = (node.reality == "1") and true or false,
					public_key = node.reality_publicKey,
					short_id = node.reality_shortId
				}
			}
		end

		local mux = nil
		if node.mux == "1" then
			mux = {
				enabled = true,
				protocol = node.mux_type or "h2mux",
				max_connections = ( (node.tcpbrutal == "1") and 1 ) or tonumber(node.mux_concurrency) or 4,
				padding = (node.mux_padding == "1") and true or false,
				--min_streams = 4,
				--max_streams = 0,
				brutal = {
					enabled = (node.tcpbrutal == "1") and true or false,
					up_mbps = tonumber(node.tcpbrutal_up_mbps) or 10,
					down_mbps = tonumber(node.tcpbrutal_down_mbps) or 50,
				},
			}
		end

		local v2ray_transport = nil

		if node.transport == "tcp" and node.tcp_guise == "http" and (node.tcp_guise_http_host or "") ~= "" then  -- Simulate X-ray Raw (TCP) transmission
			v2ray_transport = {
				type = "http",
				host = node.tcp_guise_http_host,
				path = node.tcp_guise_http_path and (function()
						local first = node.tcp_guise_http_path[1]
						return (first == "" or not first) and "/" or first
					end)() or "/",
				idle_timeout = (node.http_h2_health_check == "1") and node.http_h2_read_idle_timeout or nil,
				ping_timeout = (node.http_h2_health_check == "1") and node.http_h2_health_check_timeout or nil,
			}
			-- TLS is not enforced. If TLS is not configured, plain HTTP 1.1 will be used.
		end

		if node.transport == "http" then
			v2ray_transport = {
				type = "http",
				host = node.http_host or {},
				path = node.http_path or "/",
				idle_timeout = (node.http_h2_health_check == "1") and node.http_h2_read_idle_timeout or nil,
				ping_timeout = (node.http_h2_health_check == "1") and node.http_h2_health_check_timeout or nil,
			}
			-- TLS is not enforced. If TLS is not configured, plain HTTP 1.1 will be used.
		end

		if node.transport == "ws" then
			v2ray_transport = {
				type = "ws",
				path = node.ws_path or "/",
				headers = (node.ws_host ~= nil) and { Host = node.ws_host } or nil,
				max_early_data = tonumber(node.ws_maxEarlyData) or nil,
				early_data_header_name = (node.ws_earlyDataHeaderName) and node.ws_earlyDataHeaderName or nil -- For compatibility with Xray-core, set it to Sec-WebSocket-Protocol. It needs to be consistent with the server.
			}
		end

		if node.transport == "httpupgrade" then
			v2ray_transport = {
				type = "httpupgrade",
				host = node.httpupgrade_host,
				path = node.httpupgrade_path or "/",
			}
		end

		if node.transport == "quic" then
			v2ray_transport = {
				type = "quic"
			}
			-- There is no additional encryption support: it's essentially re-encryption. And Xray-core is incompatible with v2ray-core here.
		end

		if node.transport == "grpc" then
			v2ray_transport = {
				type = "grpc",
				service_name = node.grpc_serviceName,
				idle_timeout = tonumber(node.grpc_idle_timeout) or nil,
				ping_timeout = tonumber(node.grpc_health_check_timeout) or nil,
				permit_without_stream = (node.grpc_permit_without_stream == "1") and true or nil,
			}
		end

		local protocol_table = nil

		if node.protocol == "socks" then
			protocol_table = {
				version = "5",
				username = (node.username and node.password) and node.username or nil,
				password = (node.username and node.password) and node.password or nil,
				udp_over_tcp = node.uot == "1" and {
					enabled = true,
					version = 2
				} or nil,
			}
		end

		if node.protocol == "http" then
			protocol_table = {
				username = (node.username and node.password) and node.username or nil,
				password = (node.username and node.password) and node.password or nil,
				path = nil,
				headers = nil,
				tls = tls
			}
		end

		if node.protocol == "shadowsocks" then
			protocol_table = {
				method = node.method or nil,
				password = node.password or "",
				plugin = (node.plugin_enabled and node.plugin) or nil,
				plugin_opts = (node.plugin_enabled and node.plugin_opts) or nil,
				udp_over_tcp = node.uot == "1" and {
					enabled = true,
					version = 2
				} or nil,
				multiplex = mux,
			}
		end

		if node.protocol == "shadowsocksr" then
			protocol_table = {
				method = node.method or nil,
				password = node.password or "",
				obfs = node.ssr_obfs,
				obfs_param = node.ssr_obfs_param,
				protocol = node.ssr_protocol,
				protocol_param = node.ssr_protocol_param,
			}
		end

		if node.protocol == "trojan" then
			protocol_table = {
				password = node.password,
				tls = tls,
				multiplex = mux,
				transport = v2ray_transport
			}
		end

		if node.protocol == "vmess" then
			protocol_table = {
				uuid = node.uuid,
				security = node.security,
				alter_id = (node.alter_id) and tonumber(node.alter_id) or 0,
				global_padding = (node.global_padding == "1") and true or false,
				authenticated_length = (node.authenticated_length == "1") and true or false,
				tls = tls,
				packet_encoding = "", -- UDP packet encoding. (Empty): Disabled. packetaddr: Supported by v2ray 5+. xudp: Supported by xray.
				multiplex = mux,
				transport = v2ray_transport,
			}
		end

		if node.protocol == "vless" then
			protocol_table = {
				uuid = node.uuid,
				flow = (node.tls == '1' and node.flow) and node.flow or nil,
				tls = tls,
				packet_encoding = "xudp", -- UDP packet encoding. (Empty): Disabled. packetaddr: Supported by v2ray 5+. xudp: Supported by xray.
				multiplex = mux,
				transport = v2ray_transport,
			}
		end

		if node.protocol == "wireguard" then
			if node.wireguard_reserved then
				local bytes = {}
				if not node.wireguard_reserved:match("[^%d,]+") then
					node.wireguard_reserved:gsub("%d+", function(b)
						bytes[#bytes + 1] = tonumber(b)
					end)
				else
					local result = api.bin.b64decode(node.wireguard_reserved)
					for i = 1, #result do
						bytes[i] = result:byte(i)
					end
				end
				node.wireguard_reserved = #bytes > 0 and bytes or nil
			end
			protocol_table = {
				system_interface = (node.wireguard_system_interface == "1") and true or false,
				interface_name = node.wireguard_interface_name,
				local_address = node.wireguard_local_address,
				private_key = node.wireguard_secret_key,
				peer_public_key = node.wireguard_public_key,
				pre_shared_key = node.wireguard_preSharedKey,
				reserved = node.wireguard_reserved,
				mtu = tonumber(node.wireguard_mtu),
			}
		end

		if node.protocol == "hysteria" then
			local server_ports = {}
			if node.hysteria_hop then
				node.hysteria_hop = string.gsub(node.hysteria_hop, "-", ":")
				for range in node.hysteria_hop:gmatch("([^,]+)") do
					if range:match("^%d+:%d+$") then
						table.insert(server_ports, range)
					end
				end
			end
			protocol_table = {
				server_ports = next(server_ports) and server_ports or nil,
				hop_interval = (function()
							if not next(server_ports) then return nil end
							local v = tonumber((node.hysteria_hop_interval or "30s"):match("^%d+"))
							return (v and v >= 5) and (v .. "s") or "30s"
						end)(),
				up_mbps = tonumber(node.hysteria_up_mbps),
				down_mbps = tonumber(node.hysteria_down_mbps),
				obfs = node.hysteria_obfs,
				auth = (node.hysteria_auth_type == "base64") and node.hysteria_auth_password or nil,
				auth_str = (node.hysteria_auth_type == "string") and node.hysteria_auth_password or nil,
				recv_window_conn = tonumber(node.hysteria_recv_window_conn),
				recv_window = tonumber(node.hysteria_recv_window),
				disable_mtu_discovery = (node.hysteria_disable_mtu_discovery == "1") and true or false,
				tls = {
					enabled = true,
					server_name = node.tls_serverName,
					insecure = (node.tls_allowInsecure == "1") and true or false,
					fragment = fragment,
					record_fragment = record_fragment,
					alpn = (node.hysteria_alpn and node.hysteria_alpn ~= "") and {
						node.hysteria_alpn
					} or nil,
					ech = {
						enabled = (node.ech == "1") and true or false,
						config = node.ech_config and split(node.ech_config:gsub("\\n", "\n"), "\n") or {},
						pq_signature_schemes_enabled = node.pq_signature_schemes_enabled and true or false,
						dynamic_record_sizing_disabled = node.dynamic_record_sizing_disabled and true or false
					}
				}
			}
		end

		if node.protocol == "shadowtls" then
			protocol_table = {
				version = tonumber(node.shadowtls_version),
				password = (node.shadowtls_version == "2" or node.shadowtls_version == "3") and node.password or nil,
				tls = tls,
			}
		end

		if node.protocol == "tuic" then
			protocol_table = {
				uuid = node.uuid,
				password = node.password,
				congestion_control = node.tuic_congestion_control or "cubic",
				udp_relay_mode = node.tuic_udp_relay_mode or "native",
				udp_over_stream = false,
				zero_rtt_handshake = (node.tuic_zero_rtt_handshake == "1") and true or false,
				heartbeat = node.tuic_heartbeat .. "s",
				tls = {
					enabled = true,
					server_name = node.tls_serverName,
					insecure = (node.tls_allowInsecure == "1") and true or false,
					fragment = fragment,
					record_fragment = record_fragment,
					alpn = (node.tuic_alpn and node.tuic_alpn ~= "") and {
						node.tuic_alpn
					} or nil,
					ech = {
						enabled = (node.ech == "1") and true or false,
						config = node.ech_config and split(node.ech_config:gsub("\\n", "\n"), "\n") or {},
						pq_signature_schemes_enabled = node.pq_signature_schemes_enabled and true or false,
						dynamic_record_sizing_disabled = node.dynamic_record_sizing_disabled and true or false
					}
				}
			}
		end

		if node.protocol == "hysteria2" then
			local server_ports = {}
			if node.hysteria2_hop then
				node.hysteria2_hop = string.gsub(node.hysteria2_hop, "-", ":")
				for range in node.hysteria2_hop:gmatch("([^,]+)") do
					if range:match("^%d+:%d+$") then
						table.insert(server_ports, range)
					end
				end
			end
			protocol_table = {
				server_ports = next(server_ports) and server_ports or nil,
				hop_interval = (function()
							if not next(server_ports) then return nil end
							local v = tonumber((node.hysteria2_hop_interval or "30s"):match("^%d+"))
							return (v and v >= 5) and (v .. "s") or "30s"
						end)(),
				up_mbps = (node.hysteria2_up_mbps and tonumber(node.hysteria2_up_mbps)) and tonumber(node.hysteria2_up_mbps) or nil,
				down_mbps = (node.hysteria2_down_mbps and tonumber(node.hysteria2_down_mbps)) and tonumber(node.hysteria2_down_mbps) or nil,
				obfs = {
					type = node.hysteria2_obfs_type,
					password = node.hysteria2_obfs_password
				},
				password = node.hysteria2_auth_password or nil,
				tls = {
					enabled = true,
					server_name = node.tls_serverName,
					insecure = (node.tls_allowInsecure == "1") and true or false,
					fragment = fragment,
					record_fragment = record_fragment,
					ech = {
						enabled = (node.ech == "1") and true or false,
						config = node.ech_config and split(node.ech_config:gsub("\\n", "\n"), "\n") or {},
						pq_signature_schemes_enabled = node.pq_signature_schemes_enabled and true or false,
						dynamic_record_sizing_disabled = node.dynamic_record_sizing_disabled and true or false
					}
				}
			}
		end

		if node.protocol == "anytls" then
			protocol_table = {
				password = (node.password and node.password ~= "") and node.password or "",
				idle_session_check_interval = "30s",
				idle_session_timeout = "30s",
				min_idle_session = 5,
				tls = tls
			}
		end

		if node.protocol == "ssh" then
			protocol_table = {
				user = (node.username and node.username ~= "") and node.username or "root",
				password = (node.password and node.password ~= "") and node.password or "",
				private_key = node.ssh_priv_key,
				private_key_passphrase = node.ssh_priv_key_pp,
				host_key = node.ssh_host_key,
				host_key_algorithms = node.ssh_host_key_algo,
				client_version = node.ssh_client_version
			}
		end

		if protocol_table then
			for key, value in pairs(protocol_table) do
				result[key] = value
			end
		end
	end
	return result
end

function gen_config_server(node)
	local outbounds = {
		{ type = "direct", tag = "direct" },
		{ type = "block", tag = "block" }
	}

	local tls = {
		enabled = true,
		certificate_path = node.tls_certificateFile,
		key_path = node.tls_keyFile,
	}

	if node.tls == "1" and node.reality == "1" then
		tls.certificate_path = nil
		tls.key_path = nil
		tls.server_name = node.reality_handshake_server
		tls.reality = {
			enabled = true,
			private_key = node.reality_private_key,
			short_id = {
				node.reality_shortId
			},
			handshake = {
				server = node.reality_handshake_server,
				server_port = tonumber(node.reality_handshake_server_port)
			}
		}
	end

	if node.tls == "1" and node.ech == "1" then
		tls.ech = {
			enabled = true,
			key = node.ech_key and split(node.ech_key:gsub("\\n", "\n"), "\n") or {},
			pq_signature_schemes_enabled = (node.pq_signature_schemes_enabled == "1") and true or false,
			dynamic_record_sizing_disabled = (node.dynamic_record_sizing_disabled == "1") and true or false,
		}
	end

	local mux = nil
	if node.mux == "1" then
		mux = {
			enabled = true,
			padding = (node.mux_padding == "1") and true or false,
			brutal = {
				enabled = (node.tcpbrutal == "1") and true or false,
				up_mbps = tonumber(node.tcpbrutal_up_mbps) or 10,
				down_mbps = tonumber(node.tcpbrutal_down_mbps) or 50,
			},
		}
	end

	local v2ray_transport = nil

	if node.transport == "http" then
		v2ray_transport = {
			type = "http",
			host = node.http_host or {},
			path = node.http_path or "/",
		}
	end

	if node.transport == "ws" then
		v2ray_transport = {
			type = "ws",
			path = node.ws_path or "/",
			headers = (node.ws_host ~= nil) and { Host = node.ws_host } or nil,
			early_data_header_name = (node.ws_earlyDataHeaderName) and node.ws_earlyDataHeaderName or nil -- For compatibility with Xray-core, set it to Sec-WebSocket-Protocol. It needs to be consistent with the server.
		}
	end

	if node.transport == "httpupgrade" then
		v2ray_transport = {
			type = "httpupgrade",
			host = node.httpupgrade_host,
			path = node.httpupgrade_path or "/",
		}
	end

	if node.transport == "quic" then
		v2ray_transport = {
			type = "quic"
		}
		-- There is no additional encryption support: it's essentially re-encryption. And Xray-core is incompatible with v2ray-core here.
	end

	if node.transport == "grpc" then
		v2ray_transport = {
			type = "grpc",
			service_name = node.grpc_serviceName,
		}
	end

	local inbound = {
		type = node.protocol,
		tag = "inbound",
		listen = (node.bind_local == "1") and "127.0.0.1" or "::",
		listen_port = tonumber(node.port),
	}

	local protocol_table = nil

	if node.protocol == "mixed" then
		protocol_table = {
			users = (node.auth == "1") and {
				{
					username = node.username,
					password = node.password
				}
			} or nil,
			set_system_proxy = false
		}
	end

	if node.protocol == "socks" then
		protocol_table = {
			users = (node.auth == "1") and {
				{
					username = node.username,
					password = node.password
				}
			} or nil
		}
	end

	if node.protocol == "http" then
		protocol_table = {
			users = (node.auth == "1") and {
				{
					username = node.username,
					password = node.password
				}
			} or nil,
			tls = (node.tls == "1") and tls or nil,
		}
	end

	if node.protocol == "shadowsocks" then
		protocol_table = {
			method = node.method,
			password = node.password,
			multiplex = mux,
		}
	end

	if node.protocol == "vmess" then
		if node.uuid then
			local users = {}
			for i = 1, #node.uuid do
				users[i] = {
					name = node.uuid[i],
					uuid = node.uuid[i],
					alterId = 0,
				}
			end
			protocol_table = {
				users = users,
				tls = (node.tls == "1") and tls or nil,
				multiplex = mux,
				transport = v2ray_transport,
			}
		end
	end

	if node.protocol == "vless" then
		if node.uuid then
			local users = {}
			for i = 1, #node.uuid do
				users[i] = {
					name = node.uuid[i],
					uuid = node.uuid[i],
					flow = node.flow,
				}
			end
			protocol_table = {
				users = users,
				tls = (node.tls == "1") and tls or nil,
				multiplex = mux,
				transport = v2ray_transport,
			}
		end
	end

	if node.protocol == "trojan" then
		if node.uuid then
			local users = {}
			for i = 1, #node.uuid do
				users[i] = {
					name = node.uuid[i],
					password = node.uuid[i],
				}
			end
			protocol_table = {
				users = users,
				tls = (node.tls == "1") and tls or nil,
				fallback = nil,
				fallback_for_alpn = nil,
				multiplex = mux,
				transport = v2ray_transport,
			}
		end
	end

	if node.protocol == "naive" then
		protocol_table = {
			users = {
				{
					username = node.username,
					password = node.password
				}
			},
			tls = tls
		}
	end

	if node.protocol == "hysteria" then
		tls.alpn = (node.hysteria_alpn and node.hysteria_alpn ~= "") and {
			node.hysteria_alpn
		} or nil
		protocol_table = {
			up = node.hysteria_up_mbps .. " Mbps",
			down = node.hysteria_down_mbps .. " Mbps",
			up_mbps = tonumber(node.hysteria_up_mbps),
			down_mbps = tonumber(node.hysteria_down_mbps),
			obfs = node.hysteria_obfs,
			users = {
				{
					name = "user1",
					auth = (node.hysteria_auth_type == "base64") and node.hysteria_auth_password or nil,
					auth_str = (node.hysteria_auth_type == "string") and node.hysteria_auth_password or nil,
				}
			},
			recv_window_conn = node.hysteria_recv_window_conn and tonumber(node.hysteria_recv_window_conn) or nil,
			recv_window_client = node.hysteria_recv_window_client and tonumber(node.hysteria_recv_window_client) or nil,
			max_conn_client = node.hysteria_max_conn_client and tonumber(node.hysteria_max_conn_client) or nil,
			disable_mtu_discovery = (node.hysteria_disable_mtu_discovery == "1") and true or false,
			tls = tls
		}
	end

	if node.protocol == "tuic" then
		tls.alpn = (node.tuic_alpn and node.tuic_alpn ~= "") and {
			node.tuic_alpn
		} or nil
		protocol_table = {
			users = {
				{
					name = "user1",
					uuid = node.uuid,
					password = node.password
				}
			},
			congestion_control = node.tuic_congestion_control or "cubic",
			zero_rtt_handshake = (node.tuic_zero_rtt_handshake == "1") and true or false,
			heartbeat = node.tuic_heartbeat .. "s",
			tls = tls
		}
	end

	if node.protocol == "hysteria2" then
		protocol_table = {
			up_mbps = (node.hysteria2_ignore_client_bandwidth ~= "1" and node.hysteria2_up_mbps and tonumber(node.hysteria2_up_mbps)) and tonumber(node.hysteria2_up_mbps) or nil,
			down_mbps = (node.hysteria2_ignore_client_bandwidth ~= "1" and node.hysteria2_down_mbps and tonumber(node.hysteria2_down_mbps)) and tonumber(node.hysteria2_down_mbps) or nil,
			obfs = {
				type = node.hysteria2_obfs_type,
				password = node.hysteria2_obfs_password
			},
			users = {
				{
					name = "user1",
					password = node.hysteria2_auth_password or nil,
				}
			},
			ignore_client_bandwidth = (node.hysteria2_ignore_client_bandwidth == "1") and true or false,
			tls = tls
		}
	end

	if node.protocol == "anytls" then
		protocol_table = {
			users = {
				{
					name = (node.username and node.username ~= "") and node.username or "sekai",
					password = node.password
				}
			},
			tls = tls,
		}
	end

	if node.protocol == "direct" then
		protocol_table = {
			network = (node.d_protocol ~= "TCP,UDP") and node.d_protocol or nil,
			override_address = node.d_address,
			override_port = tonumber(node.d_port)
		}
	end

	if protocol_table then
		for key, value in pairs(protocol_table) do
			inbound[key] = value
		end
	end

	local route = {
		rules = {
			{
				ip_cidr = { "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16" },
				outbound = (node.accept_lan == nil or node.accept_lan == "0") and "block" or "direct"
			}
		}
	}

	if node.outbound_node then
		local outbound = nil
		if node.outbound_node == "_iface" and node.outbound_node_iface then
			outbound = {
				type = "direct",
				tag = "outbound",
				bind_interface = node.outbound_node_iface,
				routing_mark = 255,
			}
			sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, node.outbound_node_iface))
		else
			local outbound_node_t = uci:get_all("passwall2", node.outbound_node)
			if node.outbound_node == "_socks" or node.outbound_node == "_http" then
				outbound_node_t = {
					type = node.type,
					protocol = node.outbound_node:gsub("_", ""),
					address = node.outbound_node_address,
					port = tonumber(node.outbound_node_port),
					username = (node.outbound_node_username and node.outbound_node_username ~= "") and node.outbound_node_username or nil,
					password = (node.outbound_node_password and node.outbound_node_password ~= "") and node.outbound_node_password or nil,
				}
			end
			outbound = require("luci.passwall2.util_sing-box").gen_outbound(nil, outbound_node_t, "outbound")
		end
		if outbound then
			route.final = "outbound"
			table.insert(outbounds, 1, outbound)
		end
	end

	local config = {
		log = {
			disabled = (not node or node.log == "0") and true or false,
			level = node.loglevel or "info",
			timestamp = true,
			--output = logfile,
		},
		inbounds = { inbound },
		outbounds = outbounds,
		route = route
	}

	for index, value in ipairs(config.outbounds) do
		for k, v in pairs(config.outbounds[index]) do
			if k:find("_") == 1 then
				config.outbounds[index][k] = nil
			end
		end
	end

	if version_ge_1_11_0 then
		-- Migrate logics
		-- https://sing-box.sagernet.org/migration/
		for i = #config.outbounds, 1, -1 do
			local value = config.outbounds[i]
			if value.type == "block" then
				-- https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions
				table.remove(config.outbounds, i)
			end
		end
		-- https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions
		for i = #config.route.rules, 1, -1 do
			local value = config.route.rules[i]
			if value.outbound == "block" then
				value.action = "reject"
				value.outbound = nil
			end
		end
	end

	return config
end

function gen_config(var)
	local flag = var["-flag"]
	local log = var["-log"] or "0"
	local loglevel = var["-loglevel"] or "warn"
	local logfile = var["-logfile"] or "/dev/null"
	local node_id = var["-node"]
	local server_host = var["-server_host"]
	local server_port = var["-server_port"]
	local tcp_proxy_way = var["-tcp_proxy_way"]
	local redir_port = var["-redir_port"]
	local local_socks_address = var["-local_socks_address"] or "0.0.0.0"
	local local_socks_port = var["-local_socks_port"]
	local local_socks_username = var["-local_socks_username"]
	local local_socks_password = var["-local_socks_password"]
	local local_http_address = var["-local_http_address"] or "0.0.0.0"
	local local_http_port = var["-local_http_port"]
	local local_http_username = var["-local_http_username"]
	local local_http_password = var["-local_http_password"]
	local dns_listen_port = var["-dns_listen_port"]
	local direct_dns_udp_server = var["-direct_dns_udp_server"]
	local direct_dns_udp_port = var["-direct_dns_udp_port"]
	local direct_dns_query_strategy = var["-direct_dns_query_strategy"]
	local direct_ipset = var["-direct_ipset"]
	local direct_nftset = var["-direct_nftset"]
	local remote_dns_udp_server = var["-remote_dns_udp_server"]
	local remote_dns_udp_port = var["-remote_dns_udp_port"]
	local remote_dns_tcp_server = var["-remote_dns_tcp_server"]
	local remote_dns_tcp_port = var["-remote_dns_tcp_port"]
	local remote_dns_doh_url = var["-remote_dns_doh_url"]
	local remote_dns_doh_host = var["-remote_dns_doh_host"]
	local remote_dns_doh_ip = var["-remote_dns_doh_ip"]
	local remote_dns_doh_port = var["-remote_dns_doh_port"]
	local remote_dns_detour = var["-remote_dns_detour"]
	local remote_dns_query_strategy = var["-remote_dns_query_strategy"]
	local remote_dns_fake = var["-remote_dns_fake"]
	local remote_dns_client_ip = var["-remote_dns_client_ip"]
	local dns_cache = var["-dns_cache"]
	local tags = var["-tags"]
	local no_run = var["-no_run"]

	local dns_domain_rules = {}
	local dns = nil
	local inbounds = {}
	local outbounds = {}
	local rule_set_table = {}
	local COMMON = {}

	local CACHE_TEXT_FILE = CACHE_PATH .. "/cache_" .. flag .. ".txt"

	local singbox_settings = uci:get_all(appname, "@global_singbox[0]") or {}

	local route = {
		rules = {},
		geoip = {
			path = singbox_settings.geoip_path or "/usr/share/singbox/geoip.db",
			download_url = singbox_settings.geoip_url or nil,
			download_detour = nil,
		},
		geosite = {
			path = singbox_settings.geosite_path or "/usr/share/singbox/geosite.db",
			download_url = singbox_settings.geosite_url or nil,
			download_detour = nil,
		},
	}

	local experimental = nil

	function rule_set_add(w)
		local result = nil
		if w and #w > 0 then
			if w:find("local:") == 1 or w:find("remote:") == 1 then
				local _type = w:sub(1, w:find(":") - 1)
				w = w:sub(w:find(":") + 1, #w)
				local format = nil
				local filename = w:sub(-w:reverse():find("/") + 1)
				local suffix = ""
				local find_doc = filename:reverse():find("%.")
				if find_doc then
					suffix = filename:sub(-find_doc + 1)
				end
				if suffix == "srs" then
					format = "binary"
				elseif suffix == "json" then
					format = "source"
				end
				if format then
					local rule_set_tag = filename:sub(1, filename:find("%.") - 1)
					if not rule_set_table[rule_set_tag] then
						local t = {
							type = _type,
							tag = rule_set_tag,
							format = format,
							path = _type == "local" and w or nil,
							url = _type == "remote" and w or nil,
							--download_detour = _type == "remote" and "",
							--update_interval = _type == "remote" and "",
						}
						rule_set_table[rule_set_tag] = t
						result = t
					end
				end
			end
		end
		return result
	end

	local node = nil
	if node_id then
		node = uci:get_all(appname, node_id)
	end

	if local_socks_port then
		local inbound = {
			type = "socks",
			tag = "socks-in",
			listen = local_socks_address,
			listen_port = tonumber(local_socks_port),
			sniff = true
		}
		if local_socks_username and local_socks_password and local_socks_username ~= "" and local_socks_password ~= "" then
			inbound.users = {
				{
					username = local_socks_username,
					password = local_socks_password
				}
			}
		end
		table.insert(inbounds, inbound)
	end

	if local_http_port then
		local inbound = {
			type = "http",
			tag = "http-in",
			listen = local_http_address,
			listen_port = tonumber(local_http_port)
		}
		if local_http_username and local_http_password and local_http_username ~= "" and local_http_password ~= "" then
			inbound.users = {
				{
					username = local_http_username,
					password = local_http_password
				}
			}
		end
		table.insert(inbounds, inbound)
	end

	if redir_port then
		local inbound_tproxy = {
			type = "tproxy",
			tag = "tproxy",
			listen = "::",
			listen_port = tonumber(redir_port),
			sniff = true,
			sniff_override_destination = (singbox_settings.sniff_override_destination == "1") and true or false
		}
		if tcp_proxy_way ~= "tproxy" then
			local inbound = {
				type = "redirect",
				tag = "redirect_tcp",
				listen = "::",
				listen_port = tonumber(redir_port),
				sniff = true,
				sniff_override_destination = (singbox_settings.sniff_override_destination == "1") and true or false,
			}
			table.insert(inbounds, inbound)

			inbound_tproxy.tag = "tproxy_udp"
			inbound_tproxy.network = "udp"
		end

		table.insert(inbounds, inbound_tproxy)
	end

	if node then
		if server_host and server_port then
			node.address = server_host
			node.port = server_port
		end

		local function gen_urltest(_node)
			local urltest_id = _node[".name"]
			local urltest_tag = "urltest-" .. urltest_id
			-- existing urltest
			for _, v in ipairs(outbounds) do
				if v.tag == urltest_tag then
					return urltest_tag
				end
			end
			-- new urltest
			local ut_nodes = _node.urltest_node
			local valid_nodes = {}
			for i = 1, #ut_nodes do
				local ut_node_id = ut_nodes[i]
				local ut_node_tag = "ut-" .. ut_node_id
				local is_new_ut_node = true
				for _, outbound in ipairs(outbounds) do
					if string.sub(outbound.tag, 1, #ut_node_tag) == ut_node_tag then
						is_new_ut_node = false
						valid_nodes[#valid_nodes + 1] = outbound.tag
						break
					end
				end
				if is_new_ut_node then
					local ut_node = uci:get_all(appname, ut_node_id)
					local outbound = gen_outbound(flag, ut_node, ut_node_tag, { fragment = singbox_settings.fragment == "1" or nil, record_fragment = singbox_settings.record_fragment == "1" or nil, run_socks_instance = not no_run })
					if outbound then
						outbound.tag = outbound.tag .. ":" .. ut_node.remarks
						table.insert(outbounds, outbound)
						valid_nodes[#valid_nodes + 1] = outbound.tag
					end
				end
			end
			if #valid_nodes == 0 then return nil end
			local outbound = {
				type = "urltest",
				tag = urltest_tag,
				outbounds = valid_nodes,
				url = _node.urltest_url or "https://www.gstatic.com/generate_204",
				interval = (api.format_go_time(_node.urltest_interval) ~= "0s") and api.format_go_time(_node.urltest_interval) or "3m",
				tolerance = (_node.urltest_tolerance and tonumber(_node.urltest_tolerance) > 0) and tonumber(_node.urltest_tolerance) or 50,
				idle_timeout = (api.format_go_time(_node.urltest_idle_timeout) ~= "0s") and api.format_go_time(_node.urltest_idle_timeout) or "30m",
				interrupt_exist_connections = (_node.urltest_interrupt_exist_connections == "true" or _node.urltest_interrupt_exist_connections == "1") and true or false
			}
			table.insert(outbounds, outbound)
			return urltest_tag
		end

		local function set_outbound_detour(node, outbound, outbounds_table, shunt_rule_name)
			if not node or not outbound or not outbounds_table then return nil end
			local default_outTag = outbound.tag
			local last_insert_outbound

			if node.shadowtls == "1" then
				local _node = {
					type = "sing-box",
					protocol = "shadowtls",
					shadowtls_version = node.shadowtls_version,
					password = (node.shadowtls_version == "2" or node.shadowtls_version == "3") and node.shadowtls_password or nil,
					address = node.address,
					port = node.port,
					tls = "1",
					tls_serverName = node.shadowtls_serverName,
					utls = node.shadowtls_utls,
					fingerprint = node.shadowtls_fingerprint
				}
				local shadowtls_outbound = gen_outbound(nil, _node, outbound.tag .. "_shadowtls")
				if shadowtls_outbound then
					last_insert_outbound = shadowtls_outbound
					outbound.detour = outbound.tag .. "_shadowtls"
					outbound.server = nil
					outbound.server_port = nil
				end
			end

			if node.chain_proxy == "1" and node.preproxy_node then
				if outbound["_flag_proxy_tag"] then
					--Ignore
				else
					local preproxy_node = uci:get_all(appname, node.preproxy_node)
					if preproxy_node then
						local preproxy_outbound = gen_outbound(nil, preproxy_node)
						if preproxy_outbound then
							preproxy_outbound.tag = preproxy_node[".name"] .. ":" .. preproxy_node.remarks
							outbound.tag = preproxy_outbound.tag .. " -> " .. outbound.tag
							outbound.detour = preproxy_outbound.tag
							last_insert_outbound = preproxy_outbound
							default_outTag = outbound.tag
						end
					end
				end
			end
			if node.chain_proxy == "2" and node.to_node then
				local to_node = uci:get_all(appname, node.to_node)
				if to_node then
					local to_outbound = gen_outbound(nil, to_node)
					if to_outbound then
						if shunt_rule_name then
							to_outbound.tag = outbound.tag
							outbound.tag = node[".name"]
						else
							to_outbound.tag = outbound.tag .. " -> " .. to_outbound.tag
						end

						to_outbound.detour = outbound.tag
						table.insert(outbounds_table, to_outbound)
						default_outTag = to_outbound.tag
					end
				end
			end
			return default_outTag, last_insert_outbound
		end

		if node.protocol == "_shunt" then
			local rules = {}

			local preproxy_rule_name = node.preproxy_enabled == "1" and "main" or nil
			local preproxy_tag = preproxy_rule_name
			local preproxy_node_id = preproxy_rule_name and node["main_node"] or nil

			local function gen_shunt_node(rule_name, _node_id)
				if not rule_name then return nil end
				if not _node_id then _node_id = node[rule_name] end
				local rule_outboundTag
				if _node_id == "_direct" then
					rule_outboundTag = "direct"
				elseif _node_id == "_blackhole" then
					rule_outboundTag = "block"
				elseif _node_id == "_default" and rule_name ~= "default" then
					rule_outboundTag = "default"
				elseif _node_id and _node_id:find("Socks_") then
					local socks_id = _node_id:sub(1 + #"Socks_")
					local socks_node = uci:get_all(appname, socks_id) or nil
					if socks_node then
						local _node = {
							type = "sing-box",
							protocol = "socks",
							address = "127.0.0.1",
							port = socks_node.port,
							uot = "1",
						}
						local _outbound = gen_outbound(flag, _node, rule_name)
						if _outbound then
							table.insert(outbounds, _outbound)
							rule_outboundTag = _outbound.tag
						end
					end
				elseif _node_id then
					local _node = uci:get_all(appname, _node_id)
					if not _node then return nil end

					if api.is_normal_node(_node) then
						local use_proxy = preproxy_tag and node[rule_name .. "_proxy_tag"] == preproxy_rule_name and _node_id ~= preproxy_node_id
						local copied_outbound
						for index, value in ipairs(outbounds) do
							if value["_id"] == _node_id and value["_flag_proxy_tag"] == (use_proxy and preproxy_tag or nil) then
								copied_outbound = api.clone(value)
								break
							end
						end
						if copied_outbound then
							copied_outbound.tag = rule_name .. ":" .. _node.remarks
							table.insert(outbounds, copied_outbound)
							rule_outboundTag = copied_outbound.tag
						else
							if use_proxy then
								local pre_proxy = nil
								if _node.type ~= "sing-box" then
									pre_proxy = true
								end
								if pre_proxy then
									new_port = get_new_port()
									table.insert(inbounds, {
										type = "direct",
										tag = "proxy_" .. rule_name,
										listen = "127.0.0.1",
										listen_port = new_port,
										override_address = _node.address,
										override_port = tonumber(_node.port),
									})
									if _node.tls_serverName == nil then
										_node.tls_serverName = _node.address
									end
									_node.address = "127.0.0.1"
									_node.port = new_port
									table.insert(rules, 1, {
										inbound = {"proxy_" .. rule_name},
										outbound = preproxy_tag,
									})
								end
							end
							local proxy_table = {
								tag = use_proxy and preproxy_tag or nil,
								run_socks_instance = not no_run
							}
							if not proxy_table.tag then
								if singbox_settings.fragment == "1" then
									proxy_table.fragment = true
								end
								if singbox_settings.record_fragment == "1" then
									proxy_table.record_fragment = true
								end
							end
							local _outbound = gen_outbound(flag, _node, rule_name, proxy_table)
							if _outbound then
								_outbound.tag = _outbound.tag .. ":" .. _node.remarks
								rule_outboundTag, last_insert_outbound = set_outbound_detour(_node, _outbound, outbounds, rule_name)
								table.insert(outbounds, _outbound)
								if last_insert_outbound then
									table.insert(outbounds, last_insert_outbound)
								end
							end
						end
					elseif _node.protocol == "_urltest" then
						rule_outboundTag = gen_urltest(_node)
					elseif _node.protocol == "_iface" then
						if _node.iface then
							local _outbound = {
								type = "direct",
								tag = rule_name .. ":" .. _node.remarks,
								bind_interface = _node.iface,
								routing_mark = 255,
							}
							table.insert(outbounds, _outbound)
							rule_outboundTag = _outbound.tag
							sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, _node.iface))
						end
					end
				end
				return rule_outboundTag
			end

			if preproxy_tag and preproxy_node_id then
				local preproxy_outboundTag = gen_shunt_node(preproxy_rule_name, preproxy_node_id)
				if preproxy_outboundTag then
					preproxy_tag = preproxy_outboundTag
				end
			end
			--default_node
			local default_node_id = node.default_node or "_direct"
			COMMON.default_outbound_tag = gen_shunt_node("default", default_node_id)
			--shunt rule
			uci:foreach(appname, "shunt_rules", function(e)
				local outboundTag = gen_shunt_node(e[".name"])
				if outboundTag and e.remarks then
					if outboundTag == "default" then
						outboundTag = COMMON.default_outbound_tag
					end
					local protocols = nil
					if e["protocol"] and e["protocol"] ~= "" then
						protocols = {}
						string.gsub(e["protocol"], '[^' .. " " .. ']+', function(w)
							table.insert(protocols, w)
						end)
					end

					local inboundTag = nil
					if e["inbound"] and e["inbound"] ~= "" then
						inboundTag = {}
						if e["inbound"]:find("tproxy") then
							if redir_port then
								if tcp_proxy_way == "tproxy" then
									table.insert(inboundTag, "tproxy")
								else
									table.insert(inboundTag, "redirect_tcp")
									table.insert(inboundTag, "tproxy_udp")
								end
							end
						end
						if e["inbound"]:find("socks") then
							if local_socks_port then
								table.insert(inboundTag, "socks-in")
							end
						end
					end
					
					local rule = {
						inbound = inboundTag,
						outbound = outboundTag,
						protocol = protocols
					}

					if e.network then
						local network = {}
						string.gsub(e.network, '[^' .. "," .. ']+', function(w)
							table.insert(network, w)
						end)
						rule.network = network
					end

					if e.source then
						local source_geoip = {}
						local source_ip_cidr = {}
						string.gsub(e.source, '[^' .. " " .. ']+', function(w)
							if w:find("geoip") == 1 then
								table.insert(source_geoip, w)
							else
								table.insert(source_ip_cidr, w)
							end
						end)
						rule.source_geoip = #source_geoip > 0 and source_geoip or nil
						rule.source_ip_cidr = #source_ip_cidr > 0 and source_ip_cidr or nil
					end

					if e.sourcePort then
						local source_port = {}
						local source_port_range = {}
						string.gsub(e.sourcePort, '[^' .. "," .. ']+', function(w)
							if tonumber(w) and tonumber(w) >= 1 and tonumber(w) <= 65535 then
								table.insert(source_port, tonumber(w))
							else
								table.insert(source_port_range, w)
							end
						end)
						rule.source_port = #source_port > 0 and source_port or nil
						rule.source_port_range = #source_port_range > 0 and source_port_range or nil
					end

					if e.port then
						local port = {}
						local port_range = {}
						string.gsub(e.port, '[^' .. "," .. ']+', function(w)
							if tonumber(w) and tonumber(w) >= 1 and tonumber(w) <= 65535 then
								table.insert(port, tonumber(w))
							else
								table.insert(port_range, w)
							end
						end)
						rule.port = #port > 0 and port or nil
						rule.port_range = #port_range > 0 and port_range or nil
					end

					local rule_set = {}

					if e.domain_list then
						local domain_table = {
							outboundTag = outboundTag,
							domain = {},
							domain_suffix = {},
							domain_keyword = {},
							domain_regex = {},
							geosite = {},
							rule_set = {},
							invert = e.invert == "1" and true or nil
						}
						string.gsub(e.domain_list, '[^' .. "\r\n" .. ']+', function(w)
							if w:find("#") == 1 then return end
							if w:find("geosite:") == 1 then
								table.insert(domain_table.geosite, w:sub(1 + #"geosite:"))
							elseif w:find("regexp:") == 1 then
								table.insert(domain_table.domain_regex, w:sub(1 + #"regexp:"))
							elseif w:find("full:") == 1 then
								table.insert(domain_table.domain, w:sub(1 + #"full:"))
							elseif w:find("domain:") == 1 then
								table.insert(domain_table.domain_suffix, w:sub(1 + #"domain:"))
							elseif w:find("rule-set:", 1, true) == 1 or w:find("rs:") == 1 then
								w = w:sub(w:find(":") + 1, #w)
								local t = rule_set_add(w)
								if t then
									table.insert(rule_set, t.tag)
									table.insert(domain_table.rule_set, t.tag)
								end
							else
								table.insert(domain_table.domain_keyword, w)
							end
						end)
						rule.domain = #domain_table.domain > 0 and domain_table.domain or nil
						rule.domain_suffix = #domain_table.domain_suffix > 0 and domain_table.domain_suffix or nil
						rule.domain_keyword = #domain_table.domain_keyword > 0 and domain_table.domain_keyword or nil
						rule.domain_regex = #domain_table.domain_regex > 0 and domain_table.domain_regex or nil
						rule.geosite = #domain_table.geosite > 0 and domain_table.geosite or nil
						rule.rule_set = #domain_table.rule_set > 0 and domain_table.rule_set or nil

						if outboundTag then
							table.insert(dns_domain_rules, api.clone(domain_table))
						end
					end

					if e.ip_list then
						local ip_cidr = {}
						local geoip = {}
						string.gsub(e.ip_list, '[^' .. "\r\n" .. ']+', function(w)
							if w:find("#") == 1 then return end
							if w:find("geoip:") == 1 then
								table.insert(geoip, w:sub(1 + #"geoip:"))
							elseif w:find("rule-set:", 1, true) == 1 or w:find("rs:") == 1 then
								w = w:sub(w:find(":") + 1, #w)
								local t = rule_set_add(w)
								if t then
									table.insert(rule_set, t.tag)
								end
							else
								table.insert(ip_cidr, w)
							end
						end)

						rule.ip_cidr = #ip_cidr > 0 and ip_cidr or nil
						rule.geoip = #geoip > 0 and geoip or nil
					end
					rule.rule_set = #rule_set > 0 and rule_set or nil
					rule.invert = e.invert == "1" and true or nil

					table.insert(rules, rule)
				end
			end)

			for index, value in ipairs(rules) do
				table.insert(route.rules, rules[index])
			end
		elseif node.protocol == "_urltest" then
			if node.urltest_node then
				COMMON.default_outbound_tag = gen_urltest(node)
			end
		elseif node.protocol == "_iface" then
			if node.iface then
				local outbound = {
					type = "direct",
					tag = node.remarks or node_id,
					bind_interface = node.iface,
					routing_mark = 255,
				}
				table.insert(outbounds, outbound)
				COMMON.default_outbound_tag = outbound.tag
				sys.call(string.format("mkdir -p %s && touch %s/%s", api.TMP_IFACE_PATH, api.TMP_IFACE_PATH, node.iface))
			end
		else
			local outbound = gen_outbound(flag, node, nil, { fragment = singbox_settings.fragment == "1" or nil, record_fragment = singbox_settings.record_fragment == "1" or nil, run_socks_instance = not no_run })
			if outbound then
				outbound.tag = outbound.tag .. ":" .. node.remarks
				COMMON.default_outbound_tag, last_insert_outbound = set_outbound_detour(node, outbound, outbounds)
				table.insert(outbounds, outbound)
				if last_insert_outbound then
					table.insert(outbounds, last_insert_outbound)
				end
			end
		end
	end

	if COMMON.default_outbound_tag then
		route.final = COMMON.default_outbound_tag
	end

	if dns_listen_port then
		dns = {
			servers = {},
			rules = {},
			disable_cache = (dns_cache and dns_cache == "0") and true or false,
			disable_expire = false, -- Disable DNS cache expiration.
			independent_cache = false, -- Make each DNS server's cache independent for specific purposes. If enabled, it will slightly reduce performance.
			reverse_mapping = true, -- After responding to a DNS query, a reverse mapping of the IP address is stored to provide the domain name for routing purposes.
			fakeip = nil,
		}

		table.insert(dns.servers, {
			tag = "block",
			address = "rcode://success",
		})

		local remote_strategy = "prefer_ipv6"
		if remote_dns_query_strategy == "UseIPv4" then
			remote_strategy = "ipv4_only"
		elseif remote_dns_query_strategy == "UseIPv6" then
			remote_strategy = "ipv6_only"
		end

		local remote_server = {
			tag = "remote",
			address_strategy = "prefer_ipv4",
			strategy = remote_strategy,
			address_resolver = "direct",
			detour = COMMON.default_outbound_tag,
			client_subnet = (remote_dns_client_ip and remote_dns_client_ip ~= "") and remote_dns_client_ip or nil,
		}

		if remote_dns_detour == "direct" then
			remote_server.detour = "direct"
		end

		if remote_dns_udp_server then
			local server_port = tonumber(remote_dns_udp_port) or 53
			remote_server.address = "udp://" .. remote_dns_udp_server .. ":" .. server_port
		end

		if remote_dns_tcp_server then
			local server_port = tonumber(remote_dns_tcp_port) or 53
			remote_server.address = "tcp://" .. remote_dns_tcp_server .. ":" .. server_port
		end

		if remote_dns_doh_url then
			remote_server.address = remote_dns_doh_url
		end

		if remote_server.address then
			table.insert(dns.servers, remote_server)
		end

		local fakedns_tag = "remote_fakeip"
		if remote_dns_fake then
			dns.fakeip = {
				enabled = true,
				inet4_range = "198.18.0.0/16",
				inet6_range = "fc00::/18",
			}

			table.insert(dns.servers, {
				tag = fakedns_tag,
				address = "fakeip",
				strategy = remote_strategy,
			})

			if not experimental then
				experimental = {}
			end
			experimental.cache_file = {
				enabled = true,
				store_fakeip = true,
				path = CACHE_PATH .. "/singbox_" .. flag .. ".db"
			}
		end
	
		if direct_dns_udp_server then
			local domain = {}
			local nodes_domain_text = sys.exec('uci show passwall2 | grep ".address=" | cut -d "\'" -f 2 | grep "[a-zA-Z]$" | sort -u')
			string.gsub(nodes_domain_text, '[^' .. "\r\n" .. ']+', function(w)
				table.insert(domain, w)
			end)
			if #domain > 0 then
				table.insert(dns_domain_rules, 1, {
					outboundTag = "direct",
					domain = domain
				})
			end
	
			local direct_strategy = "prefer_ipv6"
			if direct_dns_query_strategy == "UseIPv4" then
				direct_strategy = "ipv4_only"
			elseif direct_dns_query_strategy == "UseIPv6" then
				direct_strategy = "ipv6_only"
			end
	
			local port = tonumber(direct_dns_udp_port) or 53
	
			table.insert(dns.servers, {
				tag = "direct",
				address = "udp://" .. direct_dns_udp_server .. ":" .. port,
				address_strategy = "prefer_ipv6",
				strategy = direct_strategy,
				detour = "direct",
			})
		end

		local default_dns_flag = "remote"
		if node_id and redir_port then
			local node = uci:get_all(appname, node_id)
			if node.protocol == "_shunt" then
				if node.default_node == "_direct" then
					default_dns_flag = "direct"
				end
			end
		else default_dns_flag = "direct"
		end
		dns.final = default_dns_flag

		-- DNS in order of shunt
		if dns_domain_rules and #dns_domain_rules > 0 then
			for index, value in ipairs(dns_domain_rules) do
				if value.outboundTag and (value.domain or value.domain_suffix or value.domain_keyword or value.domain_regex or value.geosite or value.rule_set) then
					local dns_rule = {
						server = value.outboundTag,
						domain = (value.domain and #value.domain > 0) and value.domain or nil,
						domain_suffix = (value.domain_suffix and #value.domain_suffix > 0) and value.domain_suffix or nil,
						domain_keyword = (value.domain_keyword and #value.domain_keyword > 0) and value.domain_keyword or nil,
						domain_regex = (value.domain_regex and #value.domain_regex > 0) and value.domain_regex or nil,
						geosite = (value.geosite and #value.geosite > 0) and value.geosite or nil,
						rule_set = (value.rule_set and #value.rule_set > 0) and value.rule_set or nil,
						disable_cache = false,
						invert = value.invert,
					}
					if value.outboundTag ~= "block" and value.outboundTag ~= "direct" then
						dns_rule.server = "remote"
						dns_rule.rewrite_ttl = 30
						if value.outboundTag ~= COMMON.default_outbound_tag and remote_server.address and remote_dns_detour ~= "direct" then
							local remote_dns_server = api.clone(remote_server)
							remote_dns_server.tag = value.outboundTag
							remote_dns_server.detour = value.outboundTag
							table.insert(dns.servers, remote_dns_server)
							dns_rule.server = remote_dns_server.tag
						end
						if remote_dns_fake then
							local fakedns_dns_rule = api.clone(dns_rule)
							fakedns_dns_rule.query_type = {
								"A", "AAAA"
							}
							fakedns_dns_rule.server = fakedns_tag
							fakedns_dns_rule.disable_cache = true
							fakedns_dns_rule.client_subnet = nil
							table.insert(dns.rules, fakedns_dns_rule)
						end
					end
					table.insert(dns.rules, dns_rule)
				end
			end
		end

		if remote_dns_fake and default_dns_flag == "remote" then
			-- When default is not direct and enable fakedns, default DNS use FakeDNS.
			local fakedns_dns_rule = {
				query_type = {
					"A", "AAAA"
				},
				server = fakedns_tag,
				disable_cache = true
			}
			table.insert(dns.rules, fakedns_dns_rule)
		end
	
		table.insert(inbounds, {
			type = "direct",
			tag = "dns-in",
			listen = "127.0.0.1",
			listen_port = tonumber(dns_listen_port),
			sniff = true,
		})
		table.insert(outbounds, {
			type = "dns",
			tag = "dns-out",
		})
		table.insert(route.rules, 1, {
			protocol = "dns",
			inbound = {
				"dns-in"
			},
			outbound = "dns-out"
		})

		local content = flag .. node_id .. jsonc.stringify(route.rules)
		if api.cacheFileCompareToLogic(CACHE_TEXT_FILE, content) == false then
			--clear ipset/nftset
			if direct_ipset then
				string.gsub(direct_ipset, '[^' .. "," .. ']+', function(w)
					sys.call("ipset -q -F " .. w)
				end)
				local ipset_prefix_name = "passwall2_" .. node_id .. "_"
				local ipset_list = sys.exec("ipset list | grep 'Name: ' | grep '" .. ipset_prefix_name .. "' | awk '{print $2}'")
				string.gsub(ipset_list, '[^' .. "\r\n" .. ']+', function(w)
					sys.call("ipset -q -F " .. w)
				end)
			end
			if direct_nftset then
				string.gsub(direct_nftset, '[^' .. "," .. ']+', function(w)
					local split = api.split(w, "#")
					if #split > 3 then
						local ip_type = split[1]
						local family = split[2]
						local table_name = split[3]
						local set_name = split[4]
						sys.call(string.format("nft flush set %s %s %s 2>/dev/null", family, table_name, set_name))
					end
				end)
				local family = "inet"
				local table_name = "passwall2"
				local nftset_prefix_name = "passwall2_" .. node_id .. "_"
				local nftset_list = sys.exec("nft -a list sets | grep -E '" .. nftset_prefix_name .. "' | awk -F 'set ' '{print $2}' | awk '{print $1}'")
				string.gsub(nftset_list, '[^' .. "\r\n" .. ']+', function(w)
					sys.call(string.format("nft flush set %s %s %s 2>/dev/null", family, table_name, w))
				end)
			end
		end
	end

	if next(rule_set_table) then
		route.rule_set = {}
		for k, v in pairs(rule_set_table) do
			table.insert(route.rule_set, v)
		end
	end
	
	if inbounds or outbounds then
		local config = {
			log = {
				disabled = log == "0" and true or false,
				level = loglevel,
				timestamp = true,
				output = logfile,
			},
			dns = dns,
			inbounds = inbounds,
			outbounds = outbounds,
			route = route,
			experimental = experimental,
		}
		table.insert(outbounds, {
			type = "direct",
			tag = "direct",
			routing_mark = 255,
			domain_strategy = "prefer_ipv6",
		})
		table.insert(outbounds, {
			type = "block",
			tag = "block"
		})
		for index, value in ipairs(config.outbounds) do
			if not value["_flag_proxy_tag"] and not value.detour and value["_id"] and value.server and value.server_port and not no_run then
				sys.call(string.format("echo '%s' >> %s", value["_id"], api.TMP_PATH .. "/direct_node_list"))
			end
			for k, v in pairs(config.outbounds[index]) do
				if k:find("_") == 1 then
					config.outbounds[index][k] = nil
				end
			end
		end
		if version_ge_1_11_0 then
			-- Migrate logics
			-- https://sing-box.sagernet.org/migration/
			local endpoints = {}
			for i = #config.outbounds, 1, -1 do
				local value = config.outbounds[i]
				if value.type == "wireguard" then
					-- https://sing-box.sagernet.org/migration/#migrate-wireguard-outbound-to-endpoint
					local endpoint = {
						type = "wireguard",
						tag = value.tag,
						system = value.system_interface,
						name = value.interface_name,
						mtu = value.mtu,
						address = value.local_address,
						private_key = value.private_key,
						peers = {
							{
								address = value.server,
								port = value.server_port,
								public_key = value.peer_public_key,
								pre_shared_key = value.pre_shared_key,
								allowed_ips = {"0.0.0.0/0"},
								reserved = value.reserved
							}
						},
						domain_strategy = value.domain_strategy,
						detour = value.detour
					}
					endpoints[#endpoints + 1] = endpoint
					table.remove(config.outbounds, i)
				end
				if value.type == "block" or value.type == "dns" then
					-- https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions
					table.remove(config.outbounds, i)
				end
			end
			if #endpoints > 0 then
				config.endpoints = endpoints
			end

			-- https://sing-box.sagernet.org/migration/#migrate-legacy-special-outbounds-to-rule-actions
			for i = #config.route.rules, 1, -1 do
				local value = config.route.rules[i]
				if value.outbound == "block" then
					value.action = "reject"
					value.outbound = nil
				elseif value.outbound == "dns-out" then
					value.action = "hijack-dns"
					value.outbound = nil
				else
					value.action = "route"
				end
			end

			-- https://sing-box.sagernet.org/migration/#migrate-legacy-inbound-fields-to-rule-actions
			for i = #config.inbounds, 1, -1 do
				local value = config.inbounds[i]
				if value.sniff == true then
					table.insert(config.route.rules, 1, {
						inbound = value.tag,
						action = "sniff"
					})
					value.sniff = nil
					value.sniff_override_destination = nil
				end
				if value.domain_strategy then
					table.insert(config.route.rules, 1, {
						inbound = value.tag,
						action = "resolve",
						strategy = value.domain_strategy,
						--server = ""
					})
					value.domain_strategy = nil
				end
			end

			if config.route.final == "block" then
				config.route.final = nil
				table.insert(config.route.rules, {
					action = "reject"
				})
			end
		end
		if version_ge_1_12_0 then
			-- removed geo in version 1.12
			config.route.geoip = nil
			config.route.geosite = nil
			if config.route and config.route.rules then
				for i = #config.route.rules, 1, -1 do
					local value = config.route.rules[i]
					value.geoip = nil
					value.geosite = nil
				end
			end
			if config.dns and config.dns.rules then
				for i = #config.dns.rules, 1, -1 do
					local value = config.dns.rules[i]
					value.geosite = nil
				end
			end
		end
		return jsonc.stringify(config, 1)
	end
end

function gen_proto_config(var)
	local local_socks_address = var["-local_socks_address"] or "0.0.0.0"
	local local_socks_port = var["-local_socks_port"]
	local local_socks_username = var["-local_socks_username"]
	local local_socks_password = var["-local_socks_password"]
	local local_http_address = var["-local_http_address"] or "0.0.0.0"
	local local_http_port = var["-local_http_port"]
	local local_http_username = var["-local_http_username"]
	local local_http_password = var["-local_http_password"]
	local server_proto = var["-server_proto"]
	local server_address = var["-server_address"]
	local server_port = var["-server_port"]
	local server_username = var["-server_username"]
	local server_password = var["-server_password"]

	local inbounds = {}
	local outbounds = {}

	if local_socks_address and local_socks_port then
		local inbound = {
			type = "socks",
			tag = "socks-in",
			listen = local_socks_address,
			listen_port = tonumber(local_socks_port),
		}
		if local_socks_username and local_socks_password and local_socks_username ~= "" and local_socks_password ~= "" then
			inbound.users = {
				username = local_socks_username,
				password = local_socks_password
			}
		end
		table.insert(inbounds, inbound)
	end

	if local_http_address and local_http_port then
		local inbound = {
			type = "http",
			tag = "http-in",
			tls = nil,
			listen = local_http_address,
			listen_port = tonumber(local_http_port),
		}
		if local_http_username and local_http_password and local_http_username ~= "" and local_http_password ~= "" then
			inbound.users = {
				{
					username = local_http_username,
					password = local_http_password
				}
			}
		end
		table.insert(inbounds, inbound)
	end

	if server_proto ~= "nil" and server_address ~= "nil" and server_port ~= "nil" then
		local outbound = {
			type = server_proto,
			tag = "out",
			server = server_address,
			server_port = tonumber(server_port),
			username = (server_username and server_password) and server_username or nil,
			password = (server_username and server_password) and server_password or nil,
		}
		if outbound then table.insert(outbounds, outbound) end
	end
	
	local config = {
		log = {
			disabled = true,
			level = "warn",
			timestamp = true,
		},
		inbounds = inbounds,
		outbounds = outbounds,
	}
	return jsonc.stringify(config, 1)
end

_G.gen_config = gen_config
_G.gen_proto_config = gen_proto_config

if arg[1] then
	local func =_G[arg[1]]
	if func then
		print(func(api.get_function_args(arg)))
	end
end
